Skip to content

前端JS模块化的演进历程

JavaScript诞生之处,作为实现浏览器端交互特效的一种脚本语言,没有预料到几十年之后的今天能取得如此大的成就。 在前端工程化的今天,模块化的构建项目已经相当普遍了,它是如何一步步走到今天这样形态的呢? 本文带你探索前端工程领域的模块化演进历程,主要讲述 JavaScript 在模块化方面的发展过程。

模块化的理解

在JavaScript发展初期就是为了实现简单的⻚⾯交互逻辑,⽽如今CPU、浏览器性能得到了极⼤的提升,很多⻚⾯逻辑迁移到了客户端(表单验证等)。 随着web2.0时代的到来,Ajax技术得到⼴泛应⽤,jQuery等前端库层出不穷,前端代码⽇益膨胀,此时在JS⽅⾯就会考虑使⽤模块化规范去管理。 模块化已经发展了有⼗余年了,不同的⼯具和轮⼦层出不穷,但总结起来,它们解决的问题主要有三个:

  1. 外部模块的管理;
  2. 内部模块的组织;
  3. 模块源码到⽬标代码的编译和转换;

以下为各个⼯具或者框架的大致诞⽣时间。

诞生时间
Node.js2009年
NPM2010年
requireJS(AMD)2010年
seaJS(CMD)2011年
broswerify2011年
webpack2012年
grunt2012年
gulp2013年
react2013年
vue2014年
angular2016年
redux2015年
vite2020年
snowpack2020年

这些工具库的诞生,促进了前端工程模块化的不断向前发展。

什么是模块化

  • 将⼀个复杂的程序依据⼀定的规则(规范)封装成⼏个块(⽂件),并进⾏组合在⼀起;
  • 块的内部数据与实现是私有的, 只是向外部暴露⼀些接⼝(⽅法)与外部其它模块通信;

模块化的进化过程

全局function模式

将不同的功能封装成不同的全局函数

  • 编码:将不同的功能封装成不同的全局函数
  • 问题:污染全局命名空间, 容易引起命名冲突或数据不安全,⽽且模块成员之间看不出直接关系
js
// global function
function foo(){
 //...
}
function bar(){
 //...
}
// global function
function foo(){
 //...
}
function bar(){
 //...
}

namespace模式

简单对象封装

  • 作⽤:减少了全局变量,解决命名冲突
  • 问题:数据不安全(外部可以直接修改模块内部的数据)
js
let myModule = {
  data: 'www.github.com',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }
}
myModule.data = 'other data' //能直接修改模块内部的数据
myModule.foo() // foo() other data
let myModule = {
  data: 'www.github.com',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }
}
myModule.data = 'other data' //能直接修改模块内部的数据
myModule.foo() // foo() other data

这样的写法会暴露所有模块成员,内部状态可以被外部改写。

IIFE模式

匿名函数⾃调⽤(闭包)

  • 作⽤:数据是私有的, 外部只能通过暴露的⽅法操作
  • 编码:将数据和⾏为封装到⼀个函数内部, 通过给window添加属性来向外暴露接⼝
  • 问题:如果当前这个模块依赖另⼀个模块怎么办?
js
// my-module.js
(function (window) {
  let data = 'hello world'
  //操作数据的函数
  function foo() {
    //⽤于暴露有函数
    console.log(`foo() ${data}`)
  }
  function bar() {
    //⽤于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() //内部调⽤
  }
  function otherFun() {
    //内部私有的函数
    console.log('otherFun()')
  }
  //暴露⾏为
  window.myModule = { foo, bar } //ES6写法
})(window)
// my-module.js
(function (window) {
  let data = 'hello world'
  //操作数据的函数
  function foo() {
    //⽤于暴露有函数
    console.log(`foo() ${data}`)
  }
  function bar() {
    //⽤于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() //内部调⽤
  }
  function otherFun() {
    //内部私有的函数
    console.log('otherFun()')
  }
  //暴露⾏为
  window.myModule = { foo, bar } //ES6写法
})(window)

在 HTML 文件中,按照下面的方式导入使用

html
<script type="text/javascript">
  myModule.foo()
  myModule.bar()
  console.log(myModule.data) //undefined 不能访问模块内部数据
  myModule.data = 'xxxx' //不是修改的模块内部的data
  myModule.foo() //没有改变
</script>
<script type="text/javascript">
  myModule.foo()
  myModule.bar()
  console.log(myModule.data) //undefined 不能访问模块内部数据
  myModule.data = 'xxxx' //不是修改的模块内部的data
  myModule.foo() //没有改变
</script>

IIFE模式增强

这就是现代模块实现的基⽯

js
// my-module.js⽂件
(function (window, $) {
  let data = 'www.baidu.com'
  //操作数据的函数
  function foo() {
    //⽤于暴露有函数
    console.log(`foo() ${data}`)
    $('body').css('background', 'red')
  }
  function bar() {
    //⽤于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() //内部调⽤
  }
  function otherFun() {
    //内部私有的函数
    console.log('otherFun()')
  }
  //暴露⾏为
  window.myModule = { foo, bar }
})(window, jQuery)
// my-module.js⽂件
(function (window, $) {
  let data = 'www.baidu.com'
  //操作数据的函数
  function foo() {
    //⽤于暴露有函数
    console.log(`foo() ${data}`)
    $('body').css('background', 'red')
  }
  function bar() {
    //⽤于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() //内部调⽤
  }
  function otherFun() {
    //内部私有的函数
    console.log('otherFun()')
  }
  //暴露⾏为
  window.myModule = { foo, bar }
})(window, jQuery)

在 HTML 文件中引入使用

html
<!-- 引⼊的js必须有⼀定顺序 -->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="my-module.js"></script>
<script type="text/javascript">
  myModule.foo()
</script>
<!-- 引⼊的js必须有⼀定顺序 -->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="my-module.js"></script>
<script type="text/javascript">
  myModule.foo()
</script>

上例⼦通过jquery⽅法将⻚⾯的背景颜⾊改成红⾊,所以必须先引⼊jQuery库,就把这个库当作参数传⼊。 这样做除了保证模块的独⽴性,还使得模块之间的依赖关系变得明显。

模块化的好处

  • 避免命名冲突(减少命名空间污染);
  • 更好的分离, 按需加载;
  • 更⾼复⽤性;
  • ⾼可维护性;

引入多个script的问题

  • 请求过多

⾸先我们要依赖多个模块,那样就会发送多个请求,导致请求过多;

  • 依赖模糊

我们不知道他们的具体依赖关系是什么,也就是说很容易因为不了解他们之间的依赖关系导致加载先后顺序出错;

  • 难以维护

以上两种原因就导致了很难维护,很可能出现牵⼀发⽽动全身的情况导致项⽬出现严重的问题。 模块化固然有多个好处,然⽽⼀个⻚⾯需要引⼊多个js⽂件,就会出现以上这些问题。 ⽽这些问题可以通过模块化规范来解决,因此才有了后续的commonjs, AMD, ES6, CMD规范。

模块化规范

模块化规范,是对代码模块化组织的一种约束和公认的最佳实践。

CommonJS

NodeJS 应⽤由模块组成,采⽤ CommonJS 模块规范。每个⽂件就是⼀个模块,有⾃⼰的作⽤域。 在⼀个⽂件⾥⾯定义的变量、函数、类,都是私有的,对其他⽂件不可⻅。 在服务器端,模块的加载是运⾏时同步加载的;在浏览器端,模块需要提前编译打包处理。

特点

  • 所有代码都运⾏在模块作⽤域,不会污染全局作⽤域;
  • 模块可以多次加载,但是只会在第⼀次加载时运⾏⼀次,然后运⾏结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运⾏,必须清除缓存;
  • 模块加载的顺序,按照其在代码中出现的顺序;

基本语法

  • 暴露模块: module.exports = value 或 exports.xxx = value
  • 引⼊模块: require(xxx) ,如果是第三⽅模块,xxx为模块名;如果是⾃定义模块,xxx为模块⽂件路径

此处我们有个疑问:CommonJS暴露的模块到底是什么? CommonJS规范规定,每个模块内部,module 变量代表当前模块。 这个变量是⼀个对象,它的exports属性(即module.exports)是对外的接⼝。 加载某个模块,其实是加载该模块的module.exports属性。

js
// example.js
var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
// example.js
var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

上⾯代码通过module.exports输出变量x和函数addX。

js
var example = require('./example.js');//如果参数字符串以“./”开头,则表示加载的是⼀个位于相对路径
console.log(example.x); // 5
console.log(example.addX(1)); // 6
var example = require('./example.js');//如果参数字符串以“./”开头,则表示加载的是⼀个位于相对路径
console.log(example.x); // 5
console.log(example.addX(1)); // 6

require命令⽤于加载模块⽂件。 require命令的基本功能是,读⼊并执⾏⼀个JavaScript⽂件,然后返回该模块的exports对象。 如果没有发现指定模块,会报错

模块的加载机制

CommonJS模块的加载机制是,输⼊的是被输出的值的拷⻉。 也就是说,⼀旦输出⼀个值,模块内部的变化就影响不到这个值。 这点与ES6模块化有重⼤差异(下⽂会介绍),我们看下⾯这个例⼦:

js
// lib.js
var counter = 3;
function increaseCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: increaseCounter,
};
// lib.js
var counter = 3;
function increaseCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: increaseCounter,
};

上⾯代码输出内部变量counter和改写这个变量的内部⽅法incCounter。

js
// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;
console.log(counter); // 3
incCounter();
console.log(counter); // 3
// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;
console.log(counter); // 3
incCounter();
console.log(counter); // 3

上⾯代码说明,counter 输出以后,lib.js模块内部的变化就影响不到counter了。 这是因为 counter 是⼀个原始类型的值,会被缓存。除⾮写成⼀个函数,才能得到内部变动后的值。

服务器端的应用

基于 NodeJS 的环境,安装 npm 依赖。假定项目工程的目录结构如下:

bash
|-modules
  |-module1.js
  |-module2.js
  |-module3.js
|-app.js
|-package.json
|-modules
  |-module1.js
  |-module2.js
  |-module3.js
|-app.js
|-package.json

定义模块代码

js
//module1.js
module.exports = {
  msg: 'module1',
  foo() {
    console.log(this.msg)
  }
}
//module2.js
module.exports = function () {
  console.log('module2')
}
//module3.js
exports.foo = function () {
  console.log('foo() module3')
}
exports.ary = [1, 2, 3, 3, 2]
// app.js⽂件
// 引⼊第三⽅库,应该放置在最前⾯(数组的去重库)
let uniq = require('uniq')
let module1 = require('./modules/module1')
let module2 = require('./modules/module2')
let module3 = require('./modules/module3')
module1.foo() //module1
module2() //module2
module3.foo() //foo() module3
console.log(uniq(module3.ary)) //[ 1, 2, 3 ]
//module1.js
module.exports = {
  msg: 'module1',
  foo() {
    console.log(this.msg)
  }
}
//module2.js
module.exports = function () {
  console.log('module2')
}
//module3.js
exports.foo = function () {
  console.log('foo() module3')
}
exports.ary = [1, 2, 3, 3, 2]
// app.js⽂件
// 引⼊第三⽅库,应该放置在最前⾯(数组的去重库)
let uniq = require('uniq')
let module1 = require('./modules/module1')
let module2 = require('./modules/module2')
let module3 = require('./modules/module3')
module1.foo() //module1
module2() //module2
module3.foo() //foo() module3
console.log(uniq(module3.ary)) //[ 1, 2, 3 ]

执行 node app.js 启动应用程序。

浏览器端的应用

使⽤Browserify:Browserify 会对代码进⾏解析,整理出代码中的所有模块依赖关系, 然后把相关的模块代码都打包在⼀起,形成⼀个完整的JS⽂件,这个⽂件中不会存在 require 这类的模块化语法, 变成可以在浏览器中运⾏的普通 JS

  1. 创建项目工程结构
bash
|-js
  |-dist //打包⽣成⽂件的⽬录
  |-src //源码所在的⽬录
    |-module1.js
    |-module2.js
    |-module3.js
    |-app.js //应⽤主源⽂件
|-index.html //运⾏于浏览器上
|-package.json
  {
    "name": "browserify-demo",
    "version": "1.0.0"
  }
|-js
  |-dist //打包⽣成⽂件的⽬录
  |-src //源码所在的⽬录
    |-module1.js
    |-module2.js
    |-module3.js
    |-app.js //应⽤主源⽂件
|-index.html //运⾏于浏览器上
|-package.json
  {
    "name": "browserify-demo",
    "version": "1.0.0"
  }
  1. 下载,安装 browserifynpm install browserify --save-dev

  2. 定义模块化代码

注意:index.html⽂件要运⾏在浏览器上,需要借助browserify将app.js⽂件打包编译,如果直接在 index.html 引⼊ app.js 就会报错!

  1. 打包 JS

在根目录下运行 browserify js/src/app.js -o js/dist/bundle.js,在 dist 目录下生成打包过的bundle.js 文件

  1. 在页面引入

在 index.html 文件中引入

html
<script type="text/javascript" src="js/dist/bundle.js"></script> 1
<script type="text/javascript" src="js/dist/bundle.js"></script> 1

AMD 规范

AMD(Asynchronous Module Definition),CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执⾏后⾯的操作。 AMD规范则是⾮同步加载模块,允许指定回调函数。 由于Node.js主要⽤于服务器编程,模块⽂件⼀般都已经存在于本地硬盘,所以加载起来⽐较快,不⽤考虑⾮同步加载的⽅式,所以CommonJS规范⽐较适⽤。 但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采⽤⾮同步模式,因此浏览器端⼀般采⽤AMD规范。 此外 AMD规范 ⽐ CommonJS规范 在浏览器端实现要来着早。

基本语法

定义对外暴露的模块

js
//定义没有依赖的模块
define(function(){
  return ...; // 对外输出的模块内容
})
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
  return ...; // 对外输出的模块内容
})
//定义没有依赖的模块
define(function(){
  return ...; // 对外输出的模块内容
})
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
  return ...; // 对外输出的模块内容
})

引⼊使⽤模块:

js
require(['module1', 'module2'], function(m1, m2){
  // 使⽤m1/m2
})
require(['module1', 'module2'], function(m1, m2){
  // 使⽤m1/m2
})

AMD实现

通过⽐较是否实⽤AMD,来说明使⽤AMD实际使⽤的效果。

未使⽤AMD规范

js
// dataService.js⽂件
(function (window) {
  let msg = 'Don\'t use AMD'
  function getMsg() {
    return msg.toUpperCase()
  }
  window.dataService = { getMsg }
})(window)
// alerter.js⽂件
(function (window, dataService) {
  let name = 'tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  window.alerter = { showMsg }
})(window, dataService)
// main.js⽂件
(function (alerter) {
  alerter.showMsg()
})(alerter)
// index.html⽂件
<div><h1>Modular Demo 1: 未使⽤AMD(require.js)</h1></div>
<script type="text/javascript" src="js/modules/dataService.js"></script>
<script type="text/javascript" src="js/modules/alerter.js"></script>
<script type="text/javascript" src="js/main.js"></script>
// dataService.js⽂件
(function (window) {
  let msg = 'Don\'t use AMD'
  function getMsg() {
    return msg.toUpperCase()
  }
  window.dataService = { getMsg }
})(window)
// alerter.js⽂件
(function (window, dataService) {
  let name = 'tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  window.alerter = { showMsg }
})(window, dataService)
// main.js⽂件
(function (alerter) {
  alerter.showMsg()
})(alerter)
// index.html⽂件
<div><h1>Modular Demo 1: 未使⽤AMD(require.js)</h1></div>
<script type="text/javascript" src="js/modules/dataService.js"></script>
<script type="text/javascript" src="js/modules/alerter.js"></script>
<script type="text/javascript" src="js/main.js"></script>

这种⽅式缺点很明显:⾸先会发送多个请求,其次引⼊的js⽂件顺序不能搞错,否则会报错

使⽤ require.js 实现的AMD规范

RequireJS是⼀个⼯具库,主要⽤于客户端的模块管理。 它的模块管理遵守AMD规范,RequireJS的基本思想是,通过define⽅法,将代码定义为模块;通过require⽅法,实现代码的模块加载。

  1. 创建项目工程的目录结构
bash
|-js
  |-libs
    |-require.js
  |-modules
    |-alerter.js
    |-dataService.js
  |-main.js
|-index.html
|-js
  |-libs
    |-require.js
  |-modules
    |-alerter.js
    |-dataService.js
  |-main.js
|-index.html
  1. 下载require.js, 将require.js导⼊项⽬: js/libs/require.js
  2. 定义 require.js 的模块代码
js
// dataService.js⽂件
// 定义没有依赖的模块
define(() => {
  const msg = 'require.js for AMD';
  function getMsg() {
    return msg.toUpperCase();
  }
  return { getMsg }; // 暴露模块
});
// alerter.js⽂件
// 定义有依赖的模块
define(['dataService'], (dataService) => {
  const name = 'amd';
  function showMsg() {
    alert(`${dataService.getMsg()}, ${name}`);
  }
  // 暴露模块
  return { showMsg };
})(() => {
  require.config({
    baseUrl: 'js/', // 基本路径 出发点在根⽬录下
    paths: {
      // 映射: 模块标识名: 路径
      alerter: './modules/alerter', // 此处不能写成alerter.js,会报错
      dataService: './modules/dataService',
    },
  });
  require(['alerter'], (alerter) => {
    alerter.showMsg();
  });
})();

// index.html⽂件
<!DOCTYPE html>
<html>
  <head>
    <title>Modular Demo</title>
  </head>
  <body>
    <!-- 引⼊require.js并指定js主⽂件的⼊⼝ -->
    <script data-main="js/main" src="js/libs/require.js"></script>
  </body>
</html>
// dataService.js⽂件
// 定义没有依赖的模块
define(() => {
  const msg = 'require.js for AMD';
  function getMsg() {
    return msg.toUpperCase();
  }
  return { getMsg }; // 暴露模块
});
// alerter.js⽂件
// 定义有依赖的模块
define(['dataService'], (dataService) => {
  const name = 'amd';
  function showMsg() {
    alert(`${dataService.getMsg()}, ${name}`);
  }
  // 暴露模块
  return { showMsg };
})(() => {
  require.config({
    baseUrl: 'js/', // 基本路径 出发点在根⽬录下
    paths: {
      // 映射: 模块标识名: 路径
      alerter: './modules/alerter', // 此处不能写成alerter.js,会报错
      dataService: './modules/dataService',
    },
  });
  require(['alerter'], (alerter) => {
    alerter.showMsg();
  });
})();

// index.html⽂件
<!DOCTYPE html>
<html>
  <head>
    <title>Modular Demo</title>
  </head>
  <body>
    <!-- 引⼊require.js并指定js主⽂件的⼊⼝ -->
    <script data-main="js/main" src="js/libs/require.js"></script>
  </body>
</html>

总结

通过两者的⽐较,可以得出AMD模块定义的⽅法⾮常清晰,不会污染全局环境,能够清楚地显示依赖关系。 AMD模式可以⽤于浏览器环境,并且允许⾮同步加载模块,也可以根据需要动态加载模块。

CMD规范

CMD(Common Module Definition),CMD规范专⻔⽤于浏览器端,模块的加载是异步的,模块使⽤时才会加载执⾏。 CMD规范整合了 CommonJS 和 AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。

基本语法

js
//定义没有依赖的模块
define(function(require, exports, module) {
  exports.xxx = value;
  module.exports = value;
});
//定义有依赖的模块
define(function(require, exports, module){
  //引⼊依赖模块(同步)
  var module2 = require('./module2');
  //引⼊依赖模块(异步)
  require.async('./module3', function (m3) {...});
  //暴露模块
  exports.xxx = value;
});
// 引⼊使⽤的模块
define(function (require) {
  var m1 = require('./module1');
  var m4 = require('./module4');
  m1.show();
  m4.show();
})
//定义没有依赖的模块
define(function(require, exports, module) {
  exports.xxx = value;
  module.exports = value;
});
//定义有依赖的模块
define(function(require, exports, module){
  //引⼊依赖模块(同步)
  var module2 = require('./module2');
  //引⼊依赖模块(异步)
  require.async('./module3', function (m3) {...});
  //暴露模块
  exports.xxx = value;
});
// 引⼊使⽤的模块
define(function (require) {
  var m1 = require('./module1');
  var m4 = require('./module4');
  m1.show();
  m4.show();
})

CMD的实现

  1. 下载CMD规范的实现库(sea.js),并导⼊项⽬: js/libs/sea.js
  2. 创建工程目录结构
bash
|-js
  |-libs
    |-sea.js
  |-modules
    |-module1.js
    |-module2.js
    |-module3.js
    |-module4.js
    |-main.js
|-index.html
|-js
  |-libs
    |-sea.js
  |-modules
    |-module1.js
    |-module2.js
    |-module3.js
    |-module4.js
    |-main.js
|-index.html
  1. 定义sea.js的模块代码
js
// module1.js⽂件
define((require, exports, module) => {
  // 内部变量数据
  const data = 'sea.js for CMD';
  // 内部函数
  function show() {
    console.log(`module1 show() ${data}`);
  }
  // 向外暴露
  exports.show = show;
});
// module2.js⽂件
define((require, exports, module) => {
  module.exports = {
    msg: 'I am xianzao',
  };
});
// module3.js⽂件
define((require, exports, module) => {
  const API_KEY = '123abc';
  exports.API_KEY = API_KEY;
});
// module4.js⽂件
define((require, exports, module) => {
  // 引⼊依赖模块(同步)
  const module2 = require('./module2');
  function show() {
    console.log(`module4 show() ${module2.msg}`);
  }
  exports.show = show;
  // 引⼊依赖模块(异步)
  require.async('./module3', (m3) => {
    console.log(`异步引⼊依赖模块3 ${m3.API_KEY}`);
  });
});
// main.js⽂件
define((require) => {
  const m1 = require('./module1');
  const m4 = require('./module4');
  m1.show();
  m4.show();
});
// module1.js⽂件
define((require, exports, module) => {
  // 内部变量数据
  const data = 'sea.js for CMD';
  // 内部函数
  function show() {
    console.log(`module1 show() ${data}`);
  }
  // 向外暴露
  exports.show = show;
});
// module2.js⽂件
define((require, exports, module) => {
  module.exports = {
    msg: 'I am xianzao',
  };
});
// module3.js⽂件
define((require, exports, module) => {
  const API_KEY = '123abc';
  exports.API_KEY = API_KEY;
});
// module4.js⽂件
define((require, exports, module) => {
  // 引⼊依赖模块(同步)
  const module2 = require('./module2');
  function show() {
    console.log(`module4 show() ${module2.msg}`);
  }
  exports.show = show;
  // 引⼊依赖模块(异步)
  require.async('./module3', (m3) => {
    console.log(`异步引⼊依赖模块3 ${m3.API_KEY}`);
  });
});
// main.js⽂件
define((require) => {
  const m1 = require('./module1');
  const m4 = require('./module4');
  m1.show();
  m4.show();
});
  1. 在 index.html 文件中引入
html
<script type="text/javascript" src="js/libs/sea.js"></script>
<script type="text/javascript">
  seajs.use('./js/modules/main')
</script>
<script type="text/javascript" src="js/libs/sea.js"></script>
<script type="text/javascript">
  seajs.use('./js/modules/main')
</script>

AMD 和 CMD的区别

js
// CMD
define((require, exports, module) => {
  // 依赖就近书写
  const module1 = require('Module1');
  const result1 = module1.exec();
  module.exports = {
    result1,
  };
});
// AMD
define(['Module1'], (module1) => {
  const result1 = module1.exec();
  return {
    result1,
  };
});
// CMD
define((require, exports, module) => {
  // 依赖就近书写
  const module1 = require('Module1');
  const result1 = module1.exec();
  module.exports = {
    result1,
  };
});
// AMD
define(['Module1'], (module1) => {
  const result1 = module1.exec();
  return {
    result1,
  };
});

从上⾯的代码⽐较中我们可以得出AMD规范和CMD规范的区别:

  1. 对依赖的处理:
  • AMD推崇依赖前置,即通过依赖数组的⽅式提前声明当前模块的依赖;
  • CMD推崇依赖就近,在编程需要⽤到的时候通过调⽤require⽅法动态引⼊;
  1. 在本模块的对外输出:
  • AMD推崇通过返回值的⽅式对外输出;
  • CMD推崇通过给module.exports赋值的⽅式对外输出;

ES6模块化

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输⼊和输出的变量。 CommonJS 和 AMD 模块,都只能在运⾏时确定这些东⻄。⽐如,CommonJS 模块就是对象,输⼊时必须查找对象属性。

基本使用 export命令⽤于规定模块的对外接⼝,import命令⽤于输⼊其他模块提供的功能。

js
/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
  return a + b;
};
export { basicNum, add };
/** 引⽤模块 **/
import { basicNum, add } from './math';
function test(ele) {
  ele.textContent = add(99 + basicNum);
}
/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
  return a + b;
};
export { basicNum, add };
/** 引⽤模块 **/
import { basicNum, add } from './math';
function test(ele) {
  ele.textContent = add(99 + basicNum);
}

如上例所示,使⽤import命令的时候,⽤户需要知道所要加载的变量名或函数名,否则⽆法加载。 为了给⽤户提供⽅便,让他们不⽤阅读⽂档就能加载模块,就要⽤到export default命令,为模块指定默认输出。

js
// export-default.js
export default function () {
 console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
// export-default.js
export default function () {
 console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'

模块默认输出, 其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

ES6 模块与 CommonJS 模块的差异

  1. CommonJS 模块输出的是⼀个值的拷⻉,ES6 模块输出的是值的引⽤;
  2. CommonJS 模块是运⾏时加载,ES6 模块是编译时输出接⼝;

第⼆个差异是因为 CommonJS 加载的是⼀个对象(即module.exports属性),该对象只有在脚本运⾏完才会⽣成。 ⽽ ES6 模块不是对象,它的对外接⼝只是⼀种静态定义,在代码静态解析阶段就会⽣成。 下⾯重点解释第⼀个差异,我们还是举上⾯那个CommonJS模块的加载机制例⼦:

js
// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

ES6 模块的运⾏机制与 CommonJS 不⼀样。ES6 模块是动态引⽤,并且不会缓存值,模块⾥⾯的变量绑定其所在的模块。

现在的大部分浏览器已经支持ES-module的规范了。

定义ES6的模块

js
// add.js
export default function add(x, y) {
  return x + y;
}
// minus.js
export default function minus(x, y) {
  return x - y;
}
// index.js
export { default as add } from './add.js';
export { default as minus } from './minus.js';
// add.js
export default function add(x, y) {
  return x + y;
}
// minus.js
export default function minus(x, y) {
  return x - y;
}
// index.js
export { default as add } from './add.js';
export { default as minus } from './minus.js';

直接在 index.html 页面中使用 ES-Module 的规范

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ES module in browser</title>
</head>
<body>
  <p>test ES-Module </p>
  <script type="module">
    import {add, minus} from './js/index.js'
    const x = 4;
    const y = 5;
    console.log('test add function: ', add(x, y));
    console.log('test minus function: ', minus(x, y));
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ES module in browser</title>
</head>
<body>
  <p>test ES-Module </p>
  <script type="module">
    import {add, minus} from './js/index.js'
    const x = 4;
    const y = 5;
    console.log('test add function: ', add(x, y));
    console.log('test minus function: ', minus(x, y));
  </script>
</body>
</html>

注意这里 <script type="module">,要将脚本的类型指定为:module

UMD规范

UMD(Universal Module Definition),是⼀种javascript通⽤模块定义规范,让你的模块能在javascript所有运⾏环境中发挥作⽤。意味着要同时满⾜CommonJS, AMD, CMD的标准,以下为实现:

js
(function (root, factory) {
  if (typeof module === 'object' && typeof module.exports === 'object') {
    console.log('是commonjs模块规范,nodejs环境');
    module.exports = factory();
  } else if (typeof define === 'function' && define.amd) {
    console.log('是AMD模块规范,如require.js');
    define(factory);
  } else if (typeof define === 'function' && define.cmd) {
    console.log('是CMD模块规范,如sea.js');
    define((require, exports, module) => {
      module.exports = factory();
    });
  } else {
    console.log('没有模块环境,直接挂载在全局对象上');
    root.umdModule = factory();
  }
}(this, () => ({
  name: '我是⼀个 UMD 模块',
})));
(function (root, factory) {
  if (typeof module === 'object' && typeof module.exports === 'object') {
    console.log('是commonjs模块规范,nodejs环境');
    module.exports = factory();
  } else if (typeof define === 'function' && define.amd) {
    console.log('是AMD模块规范,如require.js');
    define(factory);
  } else if (typeof define === 'function' && define.cmd) {
    console.log('是CMD模块规范,如sea.js');
    define((require, exports, module) => {
      module.exports = factory();
    });
  } else {
    console.log('没有模块环境,直接挂载在全局对象上');
    root.umdModule = factory();
  }
}(this, () => ({
  name: '我是⼀个 UMD 模块',
})));

总结

  1. CommonJS规范主要⽤于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决⽅案;
  2. AMD规范在浏览器环境中异步加载模块,⽽且可以并⾏加载多个模块。不过,AMD规范开发成本⾼,代码的阅读和书写⽐较困难,模块定义⽅式的语义不顺畅;
  3. CMD规范与AMD规范很相似,都⽤于浏览器编程,依赖就近,延迟执⾏,可以很容易在 Node.js 中运⾏;
  4. ES6 在语⾔标准的层⾯上,实现了模块功能,⽽且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通⽤的模块解决⽅案;
  5. UMD为同时满⾜CommonJS, AMD, CMD标准的实现;

相关链接